﻿using UnityEngine;
using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Serialization;
using System.Reflection;

namespace Hont.BlazingNodePackage
{
    public static class BlazingNodeHelper
    {
        public static Document LoadFileFromResources(string resourcesPath)
        {
            var textAsset = Resources.Load<TextAsset>(resourcesPath);
            var po = XmlDeserialize<DocumentPO>(textAsset.text);

            return ConvertToDocument("", po);
        }

        public static DocumentPO ConvertToDocumentPO(Document document)
        {
            var result = new DocumentPO();

            result.ParametersArray = document.ParametersList.Select(m => new SerializableInterface<IParameter>(m)).ToArray();

            result.GraphPOList = new List<GraphPO>();
            ConvertToGraphPO(document, document.RootGraph, ref result.GraphPOList);

            var rootGraph = result.GraphPOList.Find(m => m.GUID == document.RootGraph.GUID);
            result.RootGraph = rootGraph;

            return result;
        }

        static void ConvertToGraphPO(Document document, Graph graph, ref List<GraphPO> graphPOList)
        {
            var nodePOList = new List<NodePO>();
            var portPOList = new List<PortPO>();

            Action<NodePO, List<Port>, Action<PortPO>> processPort = (host, list, selection) =>
            {
                foreach (var port in list)
                {
                    var portPO = new PortPO();
                    portPO.GUID = port.GUID;
                    portPO.HostGUID = host.Node.Source.GUID;

                    if (port.ConnectPort != null)
                        portPO.ConnectPortGUID = port.ConnectPort.GUID;

                    if (selection != null)
                        selection(portPO);
                }
            };

            foreach (var item in graph.NodeList)
            {
                var nodePO = new NodePO();

                nodePO.Node = new SerializableInterface<INode>(item);

                nodePO.LinkedParameterGUID = item.LinkedParameter == null || document.ParametersList.Count == 0
                    ? 0
                    : document.ParametersList.FirstOrDefault(m => m == item.LinkedParameter).GUID;

                processPort(nodePO, item.InPortList, (portPO) =>
                {
                    nodePO.InPortGUIDList.Add(portPO.GUID);
                    portPOList.Add(portPO);
                });

                processPort(nodePO, item.OutPortList, (portPO) =>
                {
                    nodePO.OutPortGUIDList.Add(portPO.GUID);
                    portPOList.Add(portPO);
                });

                if (item.SubGraph != null)
                {
                    nodePO.SubGraphGUID = item.SubGraph.GUID;
                    ConvertToGraphPO(document, item.SubGraph, ref graphPOList);
                }

                nodePOList.Add(nodePO);
            }

            var graphPO = new GraphPO();
            graphPO.GUID = graph.GUID;
            graphPO.NodeList = nodePOList;
            graphPO.PortList = portPOList;
            graphPO.InNodeGUID = graph.InNode.GUID;
            graphPO.OutNodeGUID = graph.OutNode == null ? 0 : graph.OutNode.GUID;

            graphPOList.Add(graphPO);
        }

        public static Document ConvertToDocument(string fileName, DocumentPO documentPO)
        {
            var result = new Document();
            result.Name = fileName;
            result.ParametersList = documentPO.ParametersArray.Select(m => m.Source).ToList();

            var graphList = new List<Graph>();

            result.RootNode = new NodeGroup("Root");
            var rootGraph = ConvertToGraph(
                documentPO.RootGraph
                , result.ParametersList
                , documentPO.GraphPOList
                , ref graphList);

            rootGraph.ParentNode = result.RootNode;
            result.RootNode.SubGraph = rootGraph;

            var ports = graphList.SelectMany(m => m.NodeList).SelectMany(m => m.InPortList.Concat(m.OutPortList));

            for (int i = 0; i < graphList.Count; i++)
            {
                var item = graphList[i];

                var po = documentPO.GraphPOList.Find(m => m.GUID == item.GUID);

                ConnectGraphPorts(item, po, ports);
            }

            return result;
        }

        static void ConnectGraphPorts(Graph graph, GraphPO graphPO, IEnumerable<Port> ports)
        {
            foreach (var node in graph.NodeList)
            {
                Action<INode, List<Port>> process = (host, portList) =>
                {
                    foreach (var port in portList)
                    {
                        port.Host = host;

                        var po = graphPO.PortList.Find(m => m.GUID == port.GUID);

                        if (po != null)
                        {
                            var connectPort = po.ConnectPortGUID != 0 ?
                                ports.FirstOrDefault(m => m.GUID == po.ConnectPortGUID)
                                : null;

                            port.ConnectPort = connectPort;
                        }
                    }
                };

                process(node, node.InPortList);
                process(node, node.OutPortList);
            }
        }

        static Graph ConvertToGraph(GraphPO graphPO, List<IParameter> parametersList, List<GraphPO> graphPOList, ref List<Graph> graphList)
        {
            var result = new Graph();
            result.GUID = graphPO.GUID;

            foreach (var item in graphPO.NodeList)
            {
                var sourceNode = item.Node.Source as INode;

                sourceNode.OnDeserialize();

                sourceNode.LinkedParameter = item.LinkedParameterGUID == 0
                    ? null
                    : parametersList.Find(m => m.GUID == item.LinkedParameterGUID);

                if (item.SubGraphGUID != 0)
                {
                    var subGraphPO = graphPOList.Find(m => m.GUID == item.SubGraphGUID);
                    sourceNode.SubGraph = ConvertToGraph(subGraphPO, parametersList, graphPOList, ref graphList);
                    sourceNode.SubGraph.ParentNode = sourceNode;
                }

                sourceNode.CurrentGraph = result;

                result.NodeList.Add(sourceNode);
            }

            result.InNode = result.NodeList.Find(m => m is NodeGroupIn);
            result.OutNode = result.NodeList.Find(m => m is NodeGroupOut);

            graphList.Add(result);

            return result;
        }

        public static IEnumerator ToFixedCoroutine(IEnumerator enumerator)
        {
            var parentsStack = new Stack<IEnumerator>();
            var currentEnumerator = enumerator;

            parentsStack.Push(currentEnumerator);

            while (parentsStack.Count > 0)
            {
                currentEnumerator = parentsStack.Pop();

                Func<bool> func = () =>
                {
                    var isMoveNext = currentEnumerator.MoveNext();
                    return isMoveNext;
                };

                while (func())
                {
                    var subEnumerator = currentEnumerator.Current as IEnumerator;
                    if (subEnumerator != null)
                    {
                        parentsStack.Push(currentEnumerator);
                        currentEnumerator = subEnumerator;
                    }
                    else
                    {
                        if (currentEnumerator.Current is bool && (bool)currentEnumerator.Current) continue;
                        yield return currentEnumerator.Current;
                    }
                }
            }
        }

        public static long CreateGUID()
        {
            var buffer = Guid.NewGuid().ToByteArray();
            return BitConverter.ToInt64(buffer, 0);
        }

        public static MemoryStream XmlSerializationToMemory(object obj, params Type[] extraTypeArr)
        {
            var setting = new XmlWriterSettings();
            setting.Encoding = new UTF8Encoding(false);
            setting.Indent = true;

            var xmlSer = new XmlSerializer(obj.GetType(), extraTypeArr);
            var stream = new MemoryStream();

            using (var writer = XmlWriter.Create(stream, setting))
            {
                xmlSer.Serialize(writer, obj);
            }

            return stream;
        }

        public static string XmlSerialize(object obj, params Type[] extraTypeArr)
        {
            if (obj == null) return "";

            var memoryStream = XmlSerializationToMemory(obj, extraTypeArr);

            var sr = new StreamReader(memoryStream);
            var str = Encoding.UTF8.GetString(memoryStream.ToArray());

            memoryStream.Flush();
            memoryStream.Close();
            sr.Close();

            return str;
        }

        public static T XmlDeserialize<T>(string xml, params Type[] extraTypeArr)
            where T : class
        {
            if (string.IsNullOrEmpty(xml)) return default(T);

            var xmlSer = new XmlSerializer(typeof(T), extraTypeArr);
            var reader = new StringReader(xml);
            return (T)xmlSer.Deserialize(reader);
        }

        public static void WriteAllText(string filePath, string content)
        {
            if (File.Exists(filePath)) File.Delete(filePath);
            File.AppendAllText(filePath, content);
        }

        #region --- Reflection ---

        public static FieldInfo[] GetObjectPublicFields(object obj)
        {
            return obj
                .GetType()
                .GetFields(BindingFlags.Instance | BindingFlags.Public);
        }

        public static Type GetRuntimeType(string typeFullName)
        {
            if (typeFullName == null || typeFullName.Length == 0) return null;

            var result = Type.GetType(typeFullName);

            if (result == null)
            {
                var assembly = Assembly.Load("Assembly-CSharp");
                result = assembly.GetType(typeFullName);
            }

            if (result == null)
            {
                var assembly = Assembly.Load("UnityEngine");
                result = assembly.GetType(typeFullName);
            }

            return result;
        }

        public static TInterface[] GetAssemblyInterfacesAndCreate<TInterface>(object assemblyReferenceObject)
        {
            return GetAssemblyInterfacesAndCreate<TInterface>(Assembly.GetAssembly(assemblyReferenceObject.GetType()));
        }

        public static TInterface[] GetAssemblyInterfacesAndCreate<TInterface>(Assembly assembly)
        {
            var types = GetAssemblyInterfaceTypes(assembly, typeof(TInterface));
            var result = new TInterface[types.Length];

            for (int i = 0; i < result.Length; i++)
            {
                if (types[i].IsAbstract) continue;
                if (types[i].IsGenericType) continue;

                result[i] = GetAssemblyCreateInstance<TInterface>(assembly, types[i]);
            }

            return result;
        }

        public static Type[] GetAssemblyInterfaceTypes(Assembly assembly, Type type)
        {
            var types = assembly.GetTypes()
                .Where(m => m.GetInterface(type.Name) != null)
                .ToArray();

            return types;
        }

        public static T GetAssemblyCreateInstance<T>(Assembly assembly, Type type)
        {
            return (T)assembly.CreateInstance(type.FullName);
        }

        #endregion
    }
}
